Skip to content

feat(contrib): native SwiftUI menu bar app for account quota (macOS)#605

Open
plustar35 wants to merge 2 commits into
ndycode:mainfrom
plustar35:feat/contrib-macos-menubar-app
Open

feat(contrib): native SwiftUI menu bar app for account quota (macOS)#605
plustar35 wants to merge 2 commits into
ndycode:mainfrom
plustar35:feat/contrib-macos-menubar-app

Conversation

@plustar35

@plustar35 plustar35 commented Jun 11, 2026

Copy link
Copy Markdown

What

A native macOS menu bar app (contrib/macos-app/) showing per-account Codex quota cards, as an alternative to the SwiftBar plugin in #603.

menu bar:  ⚡68%        ← active account's 5h remaining

panel (click to open, refreshes live):
┌─────────────────────────────┐
│ ● account-a          ACTIVE │
│ 5h ▓▓▓▓▓▓▓▓░  68%     4h 24m │
│ 7d ▓▓▓▓▓▓▓▓░  79%     6d 18h │
│ ● account-b            IDLE │
│ 5h ▓▓▓▓▓▓▓▓▓  90%     4h 14m │
│ updating…          [Refresh]│
└─────────────────────────────┘

Why, on top of the SwiftBar plugin

SwiftBar/xbar render the dropdown as an NSMenu, and macOS does not repaint an NSMenu while it is held open (menu tracking mode). So a probe fired on open can't visibly update the panel — you must close and reopen to see fresh numbers. That's a real wart for a "check my quota" widget.

This app uses SwiftUI MenuBarExtra with .menuBarExtraStyle(.window), which renders the panel as a window. On open it shows cached values instantly, runs a background codex-multi-auth check, and updates the cards in place when it returns. Same data source as the plugin — just a UI that can update while you're looking at it.

Contents

  • CodexQuota.swift — the app (~240 lines, no dependencies)
  • build.sh — compiles with swiftc and assembles a minimal ad-hoc-signed .app (no Xcode project required)
  • local.codex.quota.plist — optional login autostart LaunchAgent
  • README.md — build/install steps and the NSMenu-vs-window rationale

Behavior

  • Menu bar title ⚡<n>%: active account's 5h remaining (active resolved from runtime observability, falling back to the stored active index)
  • On open: instant cache render → background live check if cache >60s old → live update; explicit Refresh button
  • Reading cache costs no quota; the live check sends one minimal probe per account; rows go orange <30% and red <10%

Notes

  • macOS 13+; needs the Swift toolchain (Xcode or Command Line Tools)
  • Same local files as the plugin (~/.codex/multi-auth/, honors CODEX_MULTI_AUTH_DIR); fails soft (⚡?) on missing fields
  • contrib/ is outside the npm files whitelist, so the published package is unchanged
  • Build script verified from a clean checkout on macOS 26 / Swift 5.8

Complements rather than replaces #603 — some users prefer dropping a script into an existing SwiftBar setup, others prefer a standalone app that updates live. Happy to land only one if you'd rather not carry both.

🤖 Generated with Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

adds a native swiftui menu bar app (contrib/macos-app/) as a standalone alternative to the swiftbar plugin, showing per-account codex quota cards with live in-place updates while the panel is open.

  • CodexQuota.swift (~254 lines, no dependencies) reads the same local cache files as the swiftbar plugin and spawns codex-multi-auth check in a background process for live probing; refreshIfStale now correctly uses cache file mtime (synchronous) rather than the async-committed lastUpdated.
  • build.sh compiles with swiftc and assembles an ad-hoc-signed .app bundle; local.codex.quota.plist provides an optional login autostart launchagent with a HOME_PLACEHOLDER that users substitute via the documented sed one-liner.

Confidence Score: 3/5

the panel live-update story works correctly, but the menu bar title is frozen while the panel is closed, and a launch failure in refresh() can crash the app

the 60s timer in CodexQuotaApp is declared but never connected via onReceive — model.loadCache() is never called from it, so the menu bar percentage is stale from the moment the panel is closed. separately, try? task.run() followed by unconditional waitUntilExit() is a crash path if the process launch is denied. both issues affect the primary code file and the core user-facing behaviour described in the pr.

contrib/macos-app/CodexQuota.swift — the timer wiring in CodexQuotaApp and the try?/waitUntilExit pattern in refresh() both need attention before this lands

Important Files Changed

Filename Overview
contrib/macos-app/CodexQuota.swift core app logic; timer in CodexQuotaApp is declared but never subscribed to so the menu bar title doesn't auto-refresh while the panel is closed; try?/waitUntilExit crash path in refresh().
contrib/macos-app/build.sh compiles and assembles the .app bundle; codesign failure is intentionally silenced; no -sdk/-target flags (noted in existing thread).
contrib/macos-app/local.codex.quota.plist optional LaunchAgent plist; HOME_PLACEHOLDER substitution is manual but documented; CODEX_MULTI_AUTH_DIR gap noted in existing thread.
contrib/macos-app/README.md clear build/install docs; notes the CODEX_MULTI_AUTH_DIR/LaunchAgent caveat; claims menu bar title 'refreshes from cache on a timer' which is currently broken.
contrib/macos-app/.gitignore single-line ignore for .build/; correct.

Sequence Diagram

sequenceDiagram
    participant App as CodexQuotaApp (init)
    participant Model as QuotaModel
    participant FS as Local Cache Files
    participant View as QuotaView (onAppear)
    participant Proc as /bin/zsh codex-multi-auth check

    App->>Model: loadCache()
    Model->>FS: read cache files
    FS-->>Model: data
    Model-->>App: DispatchQueue.main.async accounts updated

    View->>Model: refreshIfStale()
    Model->>FS: cacheFileMTime()
    alt "mtime < 60s"
        Model-->>View: return cache fresh
    else "mtime >= 60s"
        Model->>Proc: Process run via DispatchQueue.global
        Proc-->>Model: waitUntilExit
        Model->>FS: loadCache
        FS-->>Model: updated data
        Model-->>View: DispatchQueue.main.async accounts updated
    end

    Note over App: tick timer 60s declared but never subscribed title won't auto-refresh
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
contrib/macos-app/CodexQuota.swift:233-234
**Dead timer — menu bar title never auto-refreshes while panel is closed**

`tick` is declared and `autoconnect()`ed, but `App.body` returns a `Scene` — you can't attach `.onReceive` to a `Scene`, and there's no such call anywhere in this file. `titleTick` is assigned nowhere in `body`. As a result, `model.loadCache()` is never called from this timer; `model.accounts` (which drives `model.titleRemaining`) only changes when the panel is opened or a refresh completes. The README says the title "refreshes from cache on a timer," but with the panel closed the percentage is frozen at whatever it was at startup/last panel close — a user glancing at the icon after an hour sees stale data. To fix, drive the periodic reload from inside `QuotaModel` (e.g., schedule an `NSTimer` in `init`) so it isn't tied to a view's `onReceive`.

### Issue 2 of 3
contrib/macos-app/CodexQuota.swift:98-99
`try? task.run()` silently discards any launch error and then falls through to `task.waitUntilExit()` unconditionally. If `run()` throws (e.g., sandbox denial, unexpected fs state), `waitUntilExit()` on an un-started `Process` hits a `preconditionFailure("task not launched")` and crashes the app. Guard the wait behind a successful launch.

```suggestion
            guard (try? task.run()) != nil else {
                DispatchQueue.main.async { self.updating = false }
                return
            }
            task.waitUntilExit()
```

### Issue 3 of 3
contrib/macos-app/CodexQuota.swift:96-97
`standardError` is discarded, so when `codex-multi-auth check` fails (missing binary, auth error, network timeout), `updating` flips back to `false`, the spinner disappears, and the numbers stay stale — with zero indication of what went wrong. capturing stderr into a `Pipe` and storing the output in the model (or at least logging it to `stderr` of the host process) would make the failure mode visible without changing the UX significantly.

```suggestion
            task.standardOutput = FileHandle.nullDevice
            let stderrPipe = Pipe()
            task.standardError = stderrPipe
```

Reviews (2): Last reviewed commit: "fix(contrib): address Greptile review on..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

A native MenuBarExtra(.window) app showing per-account 5h/7d quota cards.
Unlike the NSMenu-based SwiftBar plugin, the .window panel repaints while
open, so a probe triggered on open updates the cards in place without a
reopen.

Includes the Swift source, a no-Xcode build script (swiftc + hand-rolled
bundle, ad-hoc signed), an optional login LaunchAgent, and a README
explaining the live-update rationale. Outside the npm files whitelist, so
the published package is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@plustar35 plustar35 requested a review from ndycode as a code owner June 11, 2026 13:46
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Low-risk contrib utility: This PR adds a native macOS menu bar app for displaying account quota status as a pure UI companion to existing TypeScript quota functionality. No tests are required (contrib/ excluded from npm publishing), no data-loss risks (read-only access to local cache files), and no architectural changes to core code.

Key implementation notes:

  • Subprocess execution: The refresh() method runs codex-multi-auth check via /bin/zsh with an extended PATH to locate the CLI tool. The command is hardcoded (no injection risk) and output is discarded; no error checking on the subprocess exit code (graceful degradation).
  • File staleness detection: Uses quota-cache.json modification time rather than the optional updatedAt field to avoid async-commit race conditions (addressed in follow-up fix commit).
  • Window-based UI: Intentionally uses SwiftUI MenuBarExtra(.window) instead of menu style to show live updates while the panel remains open—differentiates from the existing SwiftBar plugin (#603).
  • Environment override: Respects CODEX_MULTI_AUTH_DIR for alternative cache locations; documented and optional in LaunchAgent plist.
  • Graceful fallbacks: All JSON file reads and field accesses use optional chaining; missing data displays "no data" or "-" rather than crashing.

The build script (swiftc-based ad-hoc codesign) and LaunchAgent plist are minimalist and well-documented. The app targets macOS 13+ and requires Swift toolchain and codex-multi-auth CLI on PATH.

Walkthrough

new macOS SwiftUI menu-bar app displays per-account Codex quota from the local codex-multi-auth cache. the app loads account data, derives remaining percentages, refreshes by running codex-multi-auth check, renders quota cards with reset timing, and provides optional autostart via LaunchAgent.

Changes

macOS Quota Display App

Layer / File(s) Summary
Quota data models and observable state
contrib/macos-app/CodexQuota.swift (lines 5–129)
QuotaModel loads quota cache from ~/.codex/multi-auth/, reconstructs accounts with five-hour and seven-day remaining percentages, derives lastUpdated from metadata, and implements refresh() to run codex-multi-auth check via zsh before reloading. includes refreshIfStale() for age-gated updates and titleRemaining computed property for the menu title.
UI views and formatting helpers
contrib/macos-app/CodexQuota.swift (lines 133–228)
WindowRow renders progress and reset text per quota window; AccountCard composes two rows with account name and active status; QuotaView lists accounts, shows updating/last-updated indicators, provides refresh/quit controls, refreshes on appear when stale, and reloads cache every 30 seconds via timer. helper functions format reset times, map percentages to colors, and age Date objects to readable strings.
App entry and menu-bar wiring
contrib/macos-app/CodexQuota.swift (lines 230–254)
@main CodexQuotaApp application entry wires QuotaView with the QuotaModel observable, configures menu-bar presentation with dynamic title showing active account's remaining percentage.
Build script and launch agent
contrib/macos-app/build.sh, contrib/macos-app/local.codex.quota.plist, contrib/macos-app/.gitignore
build.sh compiles Swift source with swiftc, assembles the CodexQuota.app bundle under ~/Applications, writes Info.plist for macOS 13+, applies executable permissions, and ad-hoc codesigns. local.codex.quota.plist defines the launch agent with RunAtLoad, KeepAlive, and optional CODEX_MULTI_AUTH_DIR environment variable. .gitignore excludes .build/ directory.
Documentation
contrib/macos-app/README.md
README documents the app's purpose, UI refresh mechanics (live updates in open panel, cache staleness check, manual refresh button, color thresholds), system requirements, build/install steps, optional login autostart setup, cache location under ~/.codex/multi-auth/, soft failure for missing fields, and that contrib/ is not included in npm files whitelist.

Sequence Diagram(s)

N/A — the app's primary flow is cache reload on timer and manual refresh, not a multi-component interaction. UI rendering and state update are straightforward unidirectional bindings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

key review points:

  • concurrency risk: refresh() in range_ea4c6a822c91:line 92–129 executes codex-multi-auth check via /bin/zsh without explicit error handling if the subprocess fails or times out. no cancellation token; if refresh hangs, the app blocks. check whether zsh invocation can fail gracefully or hang the event loop.
  • cache staleness logic: refreshIfStale() checks file modification time; verify that the 60-second default threshold matches the intended user experience and that the comparison is robust across system clock changes.
  • platform coverage: SwiftUI app only targets macOS 13+. no Windows/Linux equivalent provided, but app is optional contrib; verify that README notes this is macOS-only.
  • missing test coverage: no unit tests for QuotaModel cache loading, refresh logic, or percentage calculations. no tests for the UI rendering or interaction flows. recommend regression tests for cache parsing, active account selection, and refresh state transitions.
  • json parsing: cache files are decoded directly with JSONDecoder. verify that missing or malformed files fail gracefully (code appears to use try?, so soft failure is in place) and that the UI handles nil quota windows correctly.
  • timer lifecycle: QuotaView starts a 30-second timer on appear. verify timer is cancelled on disappear to avoid multiple concurrent timers if the view is recreated.
  • environment variable override: CODEX_MULTI_AUTH_DIR documented in LaunchAgent plist but needs verification that the app actually reads and respects it; check range_ea4c6a822c91 for the env lookup code.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with feat type, (contrib) scope, and 68-char lowercase imperative summary.
Description check ✅ Passed Description covers what/why/contents/behavior/notes thoroughly, but skips the validation checklist and docs/governance checklist sections from the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread contrib/macos-app/CodexQuota.swift
Comment thread contrib/macos-app/local.codex.quota.plist

echo "Compiling…"
mkdir -p "$HERE/.build"
swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 swiftc without an explicit -sdk or -target compiles against whatever the toolchain selects by default. on machines with both Xcode and Command Line Tools pointing at different SDK roots, this can link against an unexpected SDK version or silently pick the wrong target architecture (x86_64 vs arm64). passing $(xcrun --show-sdk-path) and an explicit -target makes the build deterministic across setups.

Suggested change
swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN"
SDK=$(xcrun --show-sdk-path)
ARCH=$(uname -m)
swiftc -O -parse-as-library -sdk "$SDK" -target "${ARCH}-apple-macos13.0" "$HERE/CodexQuota.swift" -o "$BIN"
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/build.sh
Line: 14

Comment:
`swiftc` without an explicit `-sdk` or `-target` compiles against whatever the toolchain selects by default. on machines with both Xcode and Command Line Tools pointing at different SDK roots, this can link against an unexpected SDK version or silently pick the wrong target architecture (x86_64 vs arm64). passing `$(xcrun --show-sdk-path)` and an explicit `-target` makes the build deterministic across setups.

```suggestion
SDK=$(xcrun --show-sdk-path)
ARCH=$(uname -m)
swiftc -O -parse-as-library -sdk "$SDK" -target "${ARCH}-apple-macos13.0" "$HERE/CodexQuota.swift" -o "$BIN"
```

How can I resolve this? If you propose a fix, please make it concise.

- refreshIfStale: use the quota cache file's mtime as the staleness
  signal instead of the asynchronously-committed lastUpdated property.
  The old check read lastUpdated before loadCache()'s main-queue commit
  ran, and when no cache entry carried updatedAt it stayed nil, so every
  panel open fired a live probe and bypassed the 60s guard. File mtime is
  read synchronously and always present.
- LaunchAgent: document and provide a commented-out EnvironmentVariables
  block so a custom CODEX_MULTI_AUTH_DIR can be forwarded under autostart
  (LaunchAgents don't source shell profiles). README updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +233 to +234
@State private var titleTick = Date()
private let tick = Timer.publish(every: 60, on: .main, in: .common).autoconnect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dead timer — menu bar title never auto-refreshes while panel is closed

tick is declared and autoconnect()ed, but App.body returns a Scene — you can't attach .onReceive to a Scene, and there's no such call anywhere in this file. titleTick is assigned nowhere in body. As a result, model.loadCache() is never called from this timer; model.accounts (which drives model.titleRemaining) only changes when the panel is opened or a refresh completes. The README says the title "refreshes from cache on a timer," but with the panel closed the percentage is frozen at whatever it was at startup/last panel close — a user glancing at the icon after an hour sees stale data. To fix, drive the periodic reload from inside QuotaModel (e.g., schedule an NSTimer in init) so it isn't tied to a view's onReceive.

Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/CodexQuota.swift
Line: 233-234

Comment:
**Dead timer — menu bar title never auto-refreshes while panel is closed**

`tick` is declared and `autoconnect()`ed, but `App.body` returns a `Scene` — you can't attach `.onReceive` to a `Scene`, and there's no such call anywhere in this file. `titleTick` is assigned nowhere in `body`. As a result, `model.loadCache()` is never called from this timer; `model.accounts` (which drives `model.titleRemaining`) only changes when the panel is opened or a refresh completes. The README says the title "refreshes from cache on a timer," but with the panel closed the percentage is frozen at whatever it was at startup/last panel close — a user glancing at the icon after an hour sees stale data. To fix, drive the periodic reload from inside `QuotaModel` (e.g., schedule an `NSTimer` in `init`) so it isn't tied to a view's `onReceive`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +98 to +99
try? task.run()
task.waitUntilExit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 try? task.run() silently discards any launch error and then falls through to task.waitUntilExit() unconditionally. If run() throws (e.g., sandbox denial, unexpected fs state), waitUntilExit() on an un-started Process hits a preconditionFailure("task not launched") and crashes the app. Guard the wait behind a successful launch.

Suggested change
try? task.run()
task.waitUntilExit()
guard (try? task.run()) != nil else {
DispatchQueue.main.async { self.updating = false }
return
}
task.waitUntilExit()
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/CodexQuota.swift
Line: 98-99

Comment:
`try? task.run()` silently discards any launch error and then falls through to `task.waitUntilExit()` unconditionally. If `run()` throws (e.g., sandbox denial, unexpected fs state), `waitUntilExit()` on an un-started `Process` hits a `preconditionFailure("task not launched")` and crashes the app. Guard the wait behind a successful launch.

```suggestion
            guard (try? task.run()) != nil else {
                DispatchQueue.main.async { self.updating = false }
                return
            }
            task.waitUntilExit()
```

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contrib/macos-app/build.sh`:
- Line 17: Add a safety guard before the rm -rf "$APP" call: validate the APP
variable (set earlier) is non-empty and not a dangerous path ("/", "", ".",
".."), and optionally ensure it is inside the project/build workspace (e.g.,
starts with "$PWD" or a known build dir) before running rm -rf; if the check
fails, print an error and exit non-zero. Use the APP variable name and the rm
-rf invocation to locate where to insert this validation in build.sh.

In `@contrib/macos-app/CodexQuota.swift`:
- Around line 39-41: The loadCache() path must clear published state when
backing files disappear or are empty: when json("openai-codex-accounts.json")
returns nil or an empty structure, set accounts = [:] (or the empty collection
type used) and lastUpdated = nil instead of returning early; when computing
newest from the quota snapshot (the logic around newest and the cache handling
near the existing newest == 0 branch), set lastUpdated = nil whenever newest ==
0; update any cache assignment logic for json("quota-cache.json") to tolerate
missing/empty caches but not preserve prior published state. Add a
unit/integration regression test that loads successfully, then deletes or
corrupts openai-codex-accounts.json (and/or switches CODEX_MULTI_AUTH_DIR) and
asserts accounts is empty and lastUpdated is nil after calling loadCache().
- Around line 41-75: The quota lookup currently only checks the cache map byId
(byAccountId) and misses entries stored under the cache's byEmail key; update
the construction around the for-loop (where byId is read and each entry is
retrieved into entry) to first attempt lookup from byId[id], then if nil attempt
lookup from the cache's byEmail using the normalized email key (use
normalizeEmailKey(email) to match CLI behavior) so migrated/re-keyed accounts
find their quota; keep using the existing window(_:,_) and AccountQuota(...)
flow and add a regression test that loads a quota-cache.json containing only
byEmail entries (with mixed-case emails) to assert quota is found for the
account after normalization.

In `@contrib/macos-app/local.codex.quota.plist`:
- Around line 6-7: The install steps copy the plist without substituting the
home_placeholder token, producing a broken LaunchAgent (placeholder used by
local.codex.quota.plist around line with home_placeholder). Update the
instructions so the plist is written with the placeholder replaced (use the same
one-liner pattern from contrib/macos-app/README.md) — i.e., perform an inline
substitution of "home_placeholder" with "$HOME" (or the actual home path) before
placing the file into ~/Library/LaunchAgents/, then run launchctl load on the
substituted file; ensure the sequence replaces the token prior to calling
launchctl load.

In `@contrib/macos-app/README.md`:
- Line 57: Update the README entry for contrib/macos-app/build.sh to mention
that build.sh accepts an optional install path override via its first argument
(see APP="${1:-$HOME/Applications/CodexQuota.app}" in build.sh), and include an
example showing how to pass a custom path (e.g., using
/Applications/CodexQuota.app for system-wide install) so users know they can
override the default ~/Applications/CodexQuota.app.
- Line 52: The README claim that `codex-multi-auth` just needs to be "on PATH"
is misleading because contrib/macos-app/CodexQuota.swift hardcodes PATH at the
top of the spawned process to
"/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH", so installs in
custom prefixes (e.g., ~/.local/bin) will be ignored; fix by either (A) updating
the README to explicitly state the app uses that fixed PATH and list the exact
directories included (referencing CodexQuota.swift and the hardcoded PATH
string) or (B) change the app to respect the user’s environment PATH by removing
the hardcoded PATH override in CodexQuota.swift (or appending/prepending
additional common prefixes like ~/.local/bin) so subprocesses can find
user-installed `codex-multi-auth`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56b37fa3-1fb8-49e9-8fb2-693e65733646

📥 Commits

Reviewing files that changed from the base of the PR and between dae10cb and 4731711.

📒 Files selected for processing (5)
  • contrib/macos-app/.gitignore
  • contrib/macos-app/CodexQuota.swift
  • contrib/macos-app/README.md
  • contrib/macos-app/build.sh
  • contrib/macos-app/local.codex.quota.plist
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (1)
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.0-beta.3

OVERVIEW

codex-multi-auth is a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installed codex-multi-auth entrypoint handles account-management commands locally, codex-multi-auth-codex forwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.

STRUCTURE

./
├── scripts/
│   ├── codex.js              # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup
│   ├── codex-multi-auth.js   # standalone package CLI entrypoint
│   ├── codex-routing.js      # auth command and compatibility alias routing
│   ├── codex-bin-resolver.js # official Codex binary discovery
│   ├── codex-app-router.js   # persistent localhost router for packaged Codex app bind
│   └── codex-app-launcher.js # reversible user-level app launcher routing helper
├── index.ts                  # optional plugin-host runtime entry
├── lib/                      # core runtime logic (see lib/AGENTS.md)
│   ├── auth/                 # OAuth flow, PKCE, callback server
│   ├── runtime/              # Codex CLI/app integration helpers, app bind, live sync, runtime observability
│   ├── request/              # request transform, SSE, failover, backoff
│   ├── storage/              # path resolution, migrations, backups, restore, import/export
│   ├── codex-cli/            # Codex CLI state sync and writer helpers
│   ├── codex-manager/        # command modules and...

Files:

  • contrib/macos-app/local.codex.quota.plist
  • contrib/macos-app/build.sh
  • contrib/macos-app/README.md
  • contrib/macos-app/CodexQuota.swift
🪛 SwiftLint (0.63.3)
contrib/macos-app/CodexQuota.swift

[Warning] 30-30: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 55-55: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 18-18: Classes should have an explicit deinit method

(required_deinit)

🔇 Additional comments (8)
contrib/macos-app/build.sh (1)

1-16: LGTM!

Also applies to: 18-45

contrib/macos-app/local.codex.quota.plist (1)

1-5: LGTM!

Also applies to: 8-18, 20-36

contrib/macos-app/.gitignore (1)

1-1: LGTM!

contrib/macos-app/README.md (5)

1-23: LGTM!


25-35: LGTM!


37-46: LGTM!


65-76: LGTM!


78-86: LGTM!

swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN"

echo "Assembling bundle at $APP"
rm -rf "$APP"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

add a safety guard before recursive delete.

line 17 runs rm -rf on a user-provided path from line 9. this can delete unintended directories if the arg is malformed or mis-passed.

proposed fix
 APP="${1:-$HOME/Applications/CodexQuota.app}"
 BIN="$HERE/.build/CodexQuota"
 
+if [[ -z "${APP:-}" || "$APP" == "/" || "$APP" == "$HOME" || "$APP" != *.app ]]; then
+  echo "refusing unsafe app target: $APP" >&2
+  exit 1
+fi
+
 echo "Compiling…"
 mkdir -p "$HERE/.build"
 swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/build.sh` at line 17, Add a safety guard before the rm -rf
"$APP" call: validate the APP variable (set earlier) is non-empty and not a
dangerous path ("/", "", ".", ".."), and optionally ensure it is inside the
project/build workspace (e.g., starts with "$PWD" or a known build dir) before
running rm -rf; if the check fails, print an error and exit non-zero. Use the
APP variable name and the rm -rf invocation to locate where to insert this
validation in build.sh.

Comment on lines +39 to +41
func loadCache() {
guard let store = json("openai-codex-accounts.json") else { return }
let cache = json("quota-cache.json") ?? [:]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

clear published state when the backing files disappear or go empty.

line 40 returns without resetting anything, and line 80 never clears lastUpdated when the new snapshot has no timestamps. after logout, storage repair, or a CODEX_MULTI_AUTH_DIR switch, the window can keep showing the previous account cards and stale freshness text even though the source files are gone. clear accounts and lastUpdated on load failure, and nil out lastUpdated when newest == 0. please add a regression test that deletes or corrupts openai-codex-accounts.json after a successful load. the cache here is a derived local artifact, so stale ui state is worse than showing nothing. see lib/quota-cache.ts:9-36.

proposed fix
     /// Read the three local files and rebuild `accounts`. No network, no probe.
     func loadCache() {
-        guard let store = json("openai-codex-accounts.json") else { return }
+        guard let store = json("openai-codex-accounts.json") else {
+            DispatchQueue.main.async {
+                self.accounts = []
+                self.lastUpdated = nil
+            }
+            return
+        }
@@
         DispatchQueue.main.async {
             self.accounts = result
-            if newest > 0 { self.lastUpdated = Date(timeIntervalSince1970: newest / 1000) }
+            self.lastUpdated = newest > 0
+                ? Date(timeIntervalSince1970: newest / 1000)
+                : nil
         }
     }

Also applies to: 78-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/CodexQuota.swift` around lines 39 - 41, The loadCache()
path must clear published state when backing files disappear or are empty: when
json("openai-codex-accounts.json") returns nil or an empty structure, set
accounts = [:] (or the empty collection type used) and lastUpdated = nil instead
of returning early; when computing newest from the quota snapshot (the logic
around newest and the cache handling near the existing newest == 0 branch), set
lastUpdated = nil whenever newest == 0; update any cache assignment logic for
json("quota-cache.json") to tolerate missing/empty caches but not preserve prior
published state. Add a unit/integration regression test that loads successfully,
then deletes or corrupts openai-codex-accounts.json (and/or switches
CODEX_MULTI_AUTH_DIR) and asserts accounts is empty and lastUpdated is nil after
calling loadCache().

Comment on lines +41 to +75
let cache = json("quota-cache.json") ?? [:]
let observ = json("runtime-observability.json") ?? [:]
let byId = cache["byAccountId"] as? [String: Any] ?? [:]

let storeAccounts = store["accounts"] as? [[String: Any]] ?? []
let order = storeAccounts.compactMap { $0["accountId"] as? String }

var activeId = observ["lastAccountId"] as? String
if activeId == nil || !order.contains(activeId!) {
if let idx = store["activeIndex"] as? Int, idx >= 0, idx < order.count {
activeId = order[idx]
}
}

func window(_ entry: [String: Any]?, _ key: String) -> QuotaWindow? {
guard let w = entry?[key] as? [String: Any] else { return nil }
let used = (w["usedPercent"] as? NSNumber)?.intValue ?? 0
let reset = (w["resetAtMs"] as? NSNumber)?.doubleValue
return QuotaWindow(remaining: 100 - used, resetAtMs: reset)
}

var newest: Double = 0
var result: [AccountQuota] = []
for acc in storeAccounts {
guard let id = acc["accountId"] as? String else { continue }
let email = acc["email"] as? String ?? id
let name = email.split(separator: "@").first.map(String.init) ?? email
let entry = byId[id] as? [String: Any]
if let u = (entry?["updatedAt"] as? NSNumber)?.doubleValue { newest = max(newest, u) }
result.append(AccountQuota(
id: id,
name: name,
isActive: id == activeId,
fiveHour: window(entry, "primary"),
sevenDay: window(entry, "secondary")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

mirror the cache's email fallback when resolving quota entries.

line 68 only checks byAccountId. the persisted quota cache also carries byEmail, and the main cli display path resolves through that fallback before rendering quota state. with the current lookup, migrated or re-keyed accounts can render no data here even though the canonical ui still has quota for the same account. please add a regression test for a cache file that only matches on normalized email. see lib/quota-cache.ts:9-36 and lib/codex-manager/login-menu-data.ts:430-468.

proposed fix
         let cache = json("quota-cache.json") ?? [:]
         let observ = json("runtime-observability.json") ?? [:]
         let byId = cache["byAccountId"] as? [String: Any] ?? [:]
+        let byEmail = cache["byEmail"] as? [String: Any] ?? [:]
+
+        func normalizeEmailKey(_ email: String) -> String {
+            email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+        }
@@
             guard let id = acc["accountId"] as? String else { continue }
             let email = acc["email"] as? String ?? id
             let name = email.split(separator: "@").first.map(String.init) ?? email
-            let entry = byId[id] as? [String: Any]
+            let entry =
+                (byId[id] as? [String: Any]) ??
+                (byEmail[normalizeEmailKey(email)] as? [String: Any])
             if let u = (entry?["updatedAt"] as? NSNumber)?.doubleValue { newest = max(newest, u) }

as per coding guidelines, email dedup is case-insensitive via normalizeEmailKey() (trim + lowercase).

🧰 Tools
🪛 SwiftLint (0.63.3)

[Warning] 55-55: Prefer empty collection over optional collection

(discouraged_optional_collection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/CodexQuota.swift` around lines 41 - 75, The quota lookup
currently only checks the cache map byId (byAccountId) and misses entries stored
under the cache's byEmail key; update the construction around the for-loop
(where byId is read and each entry is retrieved into entry) to first attempt
lookup from byId[id], then if nil attempt lookup from the cache's byEmail using
the normalized email key (use normalizeEmailKey(email) to match CLI behavior) so
migrated/re-keyed accounts find their quota; keep using the existing
window(_:,_) and AccountQuota(...) flow and add a regression test that loads a
quota-cache.json containing only byEmail entries (with mixed-case emails) to
assert quota is found for the account after normalization.

Source: Coding guidelines

Comment on lines +6 to +7
cp contrib/macos-app/local.codex.quota.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/local.codex.quota.plist

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

fix install instructions to always replace home_placeholder.

current install steps can produce a broken launchagent because line 19 requires placeholder substitution. use the same one-liner pattern documented in contrib/macos-app/README.md.

proposed fix
-  Install:
-    cp contrib/macos-app/local.codex.quota.plist ~/Library/LaunchAgents/
-    launchctl load ~/Library/LaunchAgents/local.codex.quota.plist
+  Install:
+    sed "s|HOME_PLACEHOLDER|$HOME|" contrib/macos-app/local.codex.quota.plist \
+      > ~/Library/LaunchAgents/local.codex.quota.plist
+    launchctl load ~/Library/LaunchAgents/local.codex.quota.plist

Also applies to: 19-19

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/local.codex.quota.plist` around lines 6 - 7, The install
steps copy the plist without substituting the home_placeholder token, producing
a broken LaunchAgent (placeholder used by local.codex.quota.plist around line
with home_placeholder). Update the instructions so the plist is written with the
placeholder replaced (use the same one-liner pattern from
contrib/macos-app/README.md) — i.e., perform an inline substitution of
"home_placeholder" with "$HOME" (or the actual home path) before placing the
file into ~/Library/LaunchAgents/, then run launchctl load on the substituted
file; ensure the sequence replaces the token prior to calling launchctl load.


- macOS 13 (Ventura) or newer
- Swift toolchain — Xcode or the Command Line Tools (`xcode-select --install`)
- `codex-multi-auth` on `PATH`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

path requirement is narrower than stated.

line 52 says codex-multi-auth must be "on PATH", but contrib/macos-app/CodexQuota.swift:95 hardcodes PATH=/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH. if a user installed codex-multi-auth to a custom prefix (e.g., ~/.local/bin or a non-standard npm prefix), the subprocess won't find it even though their interactive shell PATH includes that location.

recommend either:

  • document the explicit PATH construction here so users know which install paths are supported, or
  • note that the app uses a fixed PATH and may not honor all shell configurations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/README.md` at line 52, The README claim that
`codex-multi-auth` just needs to be "on PATH" is misleading because
contrib/macos-app/CodexQuota.swift hardcodes PATH at the top of the spawned
process to "/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH", so
installs in custom prefixes (e.g., ~/.local/bin) will be ignored; fix by either
(A) updating the README to explicitly state the app uses that fixed PATH and
list the exact directories included (referencing CodexQuota.swift and the
hardcoded PATH string) or (B) change the app to respect the user’s environment
PATH by removing the hardcoded PATH override in CodexQuota.swift (or
appending/prepending additional common prefixes like ~/.local/bin) so
subprocesses can find user-installed `codex-multi-auth`.

## Build & install

```bash
contrib/macos-app/build.sh # compiles and installs ~/Applications/CodexQuota.app

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

build.sh accepts an optional install path override.

line 57 shows the default ~/Applications/CodexQuota.app, but contrib/macos-app/build.sh:6 accepts an optional first argument to override the install location (APP="${1:-$HOME/Applications/CodexQuota.app}"). consider mentioning this for users who want a different install path:

contrib/macos-app/build.sh /Applications/CodexQuota.app  # install system-wide
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/macos-app/README.md` at line 57, Update the README entry for
contrib/macos-app/build.sh to mention that build.sh accepts an optional install
path override via its first argument (see
APP="${1:-$HOME/Applications/CodexQuota.app}" in build.sh), and include an
example showing how to pass a custom path (e.g., using
/Applications/CodexQuota.app for system-wide install) so users know they can
override the default ~/Applications/CodexQuota.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant